import * as jose from 'jose';
import CryptoJS from 'crypto-js';
import LZString from 'lz-string';
import dayjs from 'dayjs';
import type { IData, IDifference, IError, IUser, IUserOv } from '@/interfaces';
import type { JWTPayload } from 'jose/dist/types/types';
import type { Metadata } from 'next';
import type { TMetadata } from '@/types';
import avatar from '@/public/images/avatar.png';
import type { StaticImageData } from 'next/image';
import type { ReadonlyURLSearchParams } from 'next/dist/client/components/navigation';

export const customData = (data: any): IData<unknown> => {
  return {
    data,
    message: 'OK',
    status: 200,
  };
};

export const customException = (status = 500, message = '未知错误'): IError => {
  return {
    status,
    message,
  };
};

export const isEmpty = (value: any) => {
  return typeof value !== 'object'
    ? false
    : Array.isArray(value)
    ? value.length === 0
    : Object.keys(value).length === 0;
};

export const nonEmpty = (value: any) => {
  return !isEmpty(value);
};

export const isPhone = (phone: string) => {
  return /^1[3-9]\d{9}$/.test(phone);
};

export const aesEncryptObj = (obj: object, key: string) => {
  return CryptoJS.AES.encrypt(JSON.stringify(obj), key).toString();
};

export const aesDecryptObj = (str: string, key: string) => {
  const bytes = CryptoJS.AES.decrypt(str, key);
  return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
};

export const aesEncryptStr = (str: string, key: string) => {
  return CryptoJS.AES.encrypt(str, key).toString();
};

export const aesDecryptStr = (str: string, key: string) => {
  const bytes = CryptoJS.AES.decrypt(str, key);
  return bytes.toString(CryptoJS.enc.Utf8);
};

export const compressToUTF16 = (str: string) => {
  return LZString.compressToUTF16(str);
};

export const decompressFromUTF16 = (str: string) => {
  return LZString.decompressFromUTF16(str);
};

export const decodeJwtExpToSeconds = (jwt: string): number | undefined => {
  return jwtExpToSeconds(jose.decodeJwt(jwt).exp);
};

export const decodeJwt = (jwt: string): JWTPayload => {
  return jose.decodeJwt(jwt);
};

export const jwtExpToSeconds = (
  exp: number | undefined,
): number | undefined => {
  if (!exp) {
    return;
  }
  return dayjs.unix(exp).diff(dayjs(), 'seconds');
};

export const range = (start: number, end: number) => {
  const length = end - start + 1;
  return Array.from({ length }, (value, index) => index + start);
};

export const isHttpOrHttps = (value: string) => {
  return value.startsWith('http') || value.startsWith('https');
};

export const getDiffData = (
  data: IDifference[] = [],
  shouldClearEmpty: boolean = false,
  shouldClearEmptyStrings: boolean = false,
) => {
  const errors: string[] = [];
  for (const { path } of data) {
    if (typeof path[0] !== 'string') {
      errors.push(`Invalid path: ${JSON.stringify(path)}.`);
    }
  }

  if (errors.length > 0) {
    throw new Error(errors.join('\n'));
  }

  const result: Record<string, any> = {};
  for (const { path, value, type } of data) {
    const key1 = path[0];
    path.forEach((key2, index) => {
      if (index === 0) {
        result[key1] = value;
      } else {
        result[key1] = typeof key2 === 'string' ? {} : [];
        if (type === 'CREATE' || type === 'CHANGE') {
          result[key1][key2] = value;
        } else if (type === 'REMOVE') {
          if (typeof key2 === 'string') {
            delete result[key1][key2];
          } else {
            result[key1].splice(key2, 1);
          }
        }
      }
    });
  }

  for (const key in result) {
    const value = result[key];
    if (isUndefined(value)) {
      delete result[key];
    } else if (Array.isArray(value) && value.length > 0) {
      result[key] = value.filter((item) => nonUndefined(item));
    }
  }

  if (shouldClearEmpty) {
    for (const key in result) {
      const value = result[key];
      if (isEmpty(value)) {
        delete result[key];
      }
    }
  }

  if (shouldClearEmptyStrings) {
    for (const key in result) {
      const value = result[key];
      if (isNull(value)) {
        delete result[key];
      }
    }
  }

  return result;
};

export const isNull = (value: any) => {
  return value === null || value === undefined || value === '';
};

export const nonNull = (value: any) => {
  return !isNull(value);
};

export const isUndefined = (value: any) => {
  return value === undefined || value === null;
};

export const nonUndefined = (value: any) => {
  return !isUndefined(value);
};

export const shuffle = (arr: any[]): any[] => {
  for (let i = 1; i < arr.length; i++) {
    const random = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[random]] = [arr[random], arr[i]];
  }
  return arr;
};

export const isNum = (val: any) => {
  return isNull(val) ? false : !isNaN(parseInt(val));
};

export const scrollToBottom = () => {
  scrollTo(0, document.body.scrollHeight);
};

export const toRelativeTime = (time: string) => {
  return dayjs().utc().local().to(dayjs(time).utc());
};

export const formatDate = (time: string) => {
  return dayjs(time).format('YYYY-MM-DD');
};

export const formatDateTime = (date: string) => {
  return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
};

export const getMetadata = (
  options: Metadata & { index?: boolean } = {
    title: 'Page',
    index: true,
  },
): Metadata => {
  const _metadata: Metadata = {
    ...options,
    title: `${options.title} - ${process.env.APP_URL_HOST}`,
    applicationName: process.env.APP_NAME,
    generator: process.env.APP_NAME,
  };

  if (!_metadata.robots) {
    if (options.index === undefined || options.index) {
      _metadata.robots = {
        index: true,
        follow: true,
      };
    } else {
      _metadata.robots = {
        index: false,
        follow: false,
      };
    }
  }

  return _metadata;
};

export const getUserAvatar = (
  user: IUserOv | IUser | null | undefined,
  metadata: TMetadata | string,
): {
  smallAvatarUrl: string | StaticImageData;
  mediumAvatarUrl: string | StaticImageData;
  largeAvatarUrl: string | StaticImageData;
} => {
  const appOssServer =
    typeof metadata === 'string'
      ? metadata
      : metadata?.env.APP_OSS_SERVER ?? '';
  const urls: {
    smallAvatarUrl: string | StaticImageData;
    mediumAvatarUrl: string | StaticImageData;
    largeAvatarUrl: string | StaticImageData;
  } = {
    smallAvatarUrl: avatar,
    mediumAvatarUrl: avatar,
    largeAvatarUrl: avatar,
  };

  if (!user) {
    return urls;
  }

  const smallAvatarUrl = user.details!.smallAvatarUrl;
  const mediumAvatarUrl = user.details!.mediumAvatarUrl;
  const largeAvatarUrl = user.details!.largeAvatarUrl;

  if (smallAvatarUrl) {
    urls.smallAvatarUrl = isHttpOrHttps(smallAvatarUrl)
      ? smallAvatarUrl
      : appOssServer + smallAvatarUrl;
  }

  if (mediumAvatarUrl) {
    urls.mediumAvatarUrl = isHttpOrHttps(mediumAvatarUrl)
      ? mediumAvatarUrl
      : appOssServer + mediumAvatarUrl;
  }

  if (largeAvatarUrl) {
    urls.largeAvatarUrl = isHttpOrHttps(largeAvatarUrl)
      ? largeAvatarUrl
      : appOssServer + largeAvatarUrl;
  }
  return urls;
};

export const getCover = (cover: string, metadata: TMetadata) => {
  return isHttpOrHttps(cover) ? cover : metadata.env.APP_OSS_SERVER + cover;
};

export const clearFormData = (form: Record<string, any> = {}): any => {
  const obj: Record<string, any> = {};
  for (const [formKey, formElement] of Object.entries(form)) {
    switch (typeof formElement) {
      case 'string':
        obj[formKey] = '';
        break;
      case 'boolean':
        obj[formKey] = false;
        break;
      case 'number':
        obj[formKey] = 0;
        break;
      case 'object':
        if (Array.isArray(formElement)) {
          obj[formKey] = [];
        } else if (formElement && Object.keys(formElement).length > 0) {
          obj[formKey] = clearFormData(formElement);
        } else {
          obj[formKey] = formElement;
        }
        break;
      default:
        obj[formKey] = formElement;
    }
  }
  return obj;
};

export const getRandomIntInclusive = (min: number, max: number) => {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
};

export const getRandomElement = (arr: unknown[]): unknown | undefined => {
  return arr[Math.floor(Math.random() * arr.length)];
};

export const decodeHtml = (html: string) => {
  const obj: Record<string, string> = {
    '&amp;': '&',
    '&lt;': '<',
    '&gt;': '>',
    '&quot;': '"',
    '&#x27;': "'",
    '&#x2F;': '/',
  };
  return html.replace(
    /&amp;|&lt;|&gt;|&quot;|&#x27;|&#x2F;/g,
    (key) => obj[key],
  );
};

export const delayedWait = async (
  timeout?: number | Function,
  callback?: () => void,
) => {
  await new Promise((resolve) => {
    setTimeout(
      () => {
        if (typeof callback === 'function') {
          callback();
        } else if (typeof timeout === 'function') {
          timeout();
        }
        resolve(true);
      },
      typeof timeout === 'number' ? timeout : 5000,
    );
  });
};

export const mergeObjects = (
  target: Record<string, any>,
  source?: Record<string, any>,
  fields?: string[],
): any => {
  if (!source) {
    return target;
  }
  if (!fields) {
    fields = Object.keys(source);
  }
  fields.forEach((field) => {
    if (nonUndefined(source[field])) {
      target[field] = source[field];
    }
  });
  return target;
};

export const toCamelCase = (str: string) => {
  const split = str.split('-');
  for (let i = 1; i < split.length; i++) {
    split[i] = split[i].charAt(0).toUpperCase() + split[i].substring(1);
  }
  return split.join('');
};

export const getRandomRgbColor = () => {
  const r = Math.floor(Math.random() * 256);
  const g = Math.floor(Math.random() * 256);
  const b = Math.floor(Math.random() * 256);
  return `rgb(${r},${g},${b})`;
};

export const generateRandomLinearGradientColor = () => {
  const angle = Math.floor(Math.random() * 361);
  return `linear-gradient(${angle}deg, ${getRandomRgbColor()}, ${getRandomRgbColor()})`;
};

export const formatCount = (count: number | null | undefined): string => {
  if (typeof count !== 'number') {
    return '0';
  }

  if (count < 1000) {
    return count + '';
  }

  let suffix = '';
  let divisor = 1;

  if (count >= 1000000000) {
    suffix = 'B';
    divisor = 1000000000;
  } else if (count >= 1000000) {
    suffix = 'M';
    divisor = 1000000;
  } else if (count >= 1000) {
    suffix = 'K';
    divisor = 1000;
  }

  return (count / divisor).toFixed(1) + suffix;
};

export const getSectionQueryParams = (
  searchParams: ReadonlyURLSearchParams,
): any | undefined => {
  if (searchParams.has('sectionId')) {
    return {
      id: parseInt(searchParams.get('sectionId')!),
    } as any;
  } else if (searchParams.has('sid')) {
    return {
      id: parseInt(searchParams.get('sid')!),
    } as any;
  }
};

export const getTagQueryParams = (
  searchParams: ReadonlyURLSearchParams,
): any | undefined => {
  if (searchParams.has('tagId')) {
    return {
      id: parseInt(searchParams.get('tagId')!),
    } as any;
  } else if (searchParams.has('tid')) {
    return {
      id: parseInt(searchParams.get('tid')!),
    } as any;
  }
};

export const getPostQueryParams = (
  searchParams: ReadonlyURLSearchParams,
): any | undefined => {
  if (searchParams.has('postId')) {
    return {
      id: parseInt(searchParams.get('postId')!),
    } as any;
  } else if (searchParams.has('pid')) {
    return {
      id: parseInt(searchParams.get('pid')!),
    } as any;
  }
};

export const getUserQueryParams = (
  searchParams: ReadonlyURLSearchParams,
): any | undefined => {
  if (searchParams.has('userId')) {
    return {
      id: parseInt(searchParams.get('userId')!),
    } as any;
  } else if (searchParams.has('uid')) {
    return {
      id: parseInt(searchParams.get('uid')!),
    } as any;
  }
};

export const parseMessage = (message: any): string => {
  if (message === undefined || message === null) {
    return '';
  }

  if (typeof message === 'object' && 'message' in message) {
    return parseMessage(message.message);
  }

  if (message instanceof Error) {
    return parseMessage(message.message);
  }

  return String(message);
};

export const isValidLink = (link: string) => {
  return /^(http|https):\/\//.test(link);
};

export const composeMiddlewares = (...funcs: Function[]) => {
  return funcs.length === 0
    ? (arg: any) => arg
    : funcs.length === 1
    ? funcs[0]
    : funcs.reduce(
        (a, b) =>
          (...args: any[]) =>
            b(a(...args)),
      );
};

export const getImageSize = (url: string, data?: {}): Promise<any> => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      resolve({ ...data, width: img.width, height: img.height });
    };
    img.onerror = () => {
      reject(new Error(`Failed to load image ${url}`));
    };
    img.src = url;
  });
};

export const filterObject = (
  obj: Record<string, any>,
  filterFields: string[],
  modifyOriginal = false,
) => {
  const newObj = modifyOriginal ? obj : { ...obj };
  Object.entries(newObj).forEach(([key]) => {
    if (filterFields.includes(key)) {
      delete newObj[key];
    }
  });
  return newObj;
};

export const parseJSON = (str?: string) => {
  if (!str) {
    return;
  }

  try {
    const parse = JSON.parse(str);
    if (Object.keys(parse).length === 0) {
      return;
    }
    return parse;
  } catch (error) {
    return;
  }
};

export const createError = (
  error: any,
  config = {
    onlyPlainObject: true,
  },
) => {
  return config.onlyPlainObject
    ? JSON.stringify(error)
    : new Error(JSON.stringify(error));
};

export const convertCamelToDash = (str: string) => {
  return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
};

export const getGivenTranslatedFields = (
  fields: string[] | Record<string, any>,
  translatedFields: Record<string, any>,
): any => {
  if (Array.isArray(fields)) {
    return fields.reduce((result, field) => {
      const { [field]: fieldValue } = translatedFields;
      return { ...result, [field]: fieldValue };
    }, {});
  }
  return translatedFields;
};

export const getQueryStrings = (
  target: Record<string, any>,
  source: ReadonlyURLSearchParams,
  filter?: string[],
  shouldDeleteFilter?: boolean,
): string => {
  const query = Object.fromEntries(source);

  if (filter && filter.length > 0) {
    const filterFn = shouldDeleteFilter
      ? (key: string) => filter.includes(key)
      : (key: string) => !filter.includes(key);

    Object.keys(query).forEach((key) => {
      if (filterFn(key)) {
        delete query[key];
      }
    });
  }

  return new URLSearchParams({ ...query, ...target }).toString();
};

export const filterNumericParams = (params: Record<string, any>) => {
  const filteredParams: Record<string, any> = {};
  for (const [key, value] of Object.entries(params)) {
    if (isNum(value)) {
      filteredParams[key] = value;
    }
  }
  return filteredParams;
};

export const splitArrayIntoGroups = (array: any[], groupSize: number) => {
  const groups = [];
  for (let i = 0; i < array.length; i += groupSize) {
    groups.push(array.slice(i, i + groupSize));
  }
  return groups;
};

export const calculateDateTimeRange = (
  value = 0,
  format = 'YYYY-MM-DDTHH:mm:ss',
) => {
  const dayUnit = 'day';
  const now = dayjs();
  let startDateTime;
  let endDateTime;
  if (value === 0) {
    startDateTime = now.startOf(dayUnit).format(format);
    endDateTime = now.endOf(dayUnit).format(format);
  } else {
    startDateTime = now
      .subtract(value, dayUnit)
      .startOf(dayUnit)
      .format(format);
    endDateTime = now.subtract(1, dayUnit).endOf(dayUnit).format(format);
  }
  return {
    startDateTime,
    endDateTime,
  };
};

export const sumArrayValues = (array: any) => {
  return array.reduce(
    (accumulator: number, currentValue: number) => accumulator + currentValue,
    0,
  );
};
